[L3H]easy_android
~~ 题目实际上不难,找到关键词跟进甚至不用管他的逻辑和架构丢给ai一把梭~~
基本操作:放进jadx中,跟进逻辑,有load加载native方法,吧lib文件中的so文件用ida打开查看逻辑
java层:
先是查看主类,发现是主类直接继承了TauriActivity这个类,疑似使用了tauri框架。(都学过java,别告诉我不知道继承是什么

什么是Tauri
https://github.com/tauri-apps/tauri/
Tauri 是一个多语言且通用的工具包,非常灵活,允许工程师开发各种类型的应用。它用于构建桌面应用程序,结合了 Rust 工具和 Webview 中渲染的 HTML。使用 Tauri 构建的应用可以包含任意数量的可选 JS API/Rust API,以便 Webview 可以通过消息传递控制系统。实际上,开发者可以扩展默认 API 以添加自己的功能,并轻松地桥接 Webview 和基于 Rust 的后端。
Tauri 应用中的用户界面目前利用 tao 作为 macOS、Windows、Linux、Android 和 iOS 的窗口处理库。为了渲染应用,Tauri 使用 WRY 库,该库提供了一致接口以系统 WebView,在 macOS & iOS 上利用 WKWebView,在 Windows 上利用 WebView2,在 Linux 和 Android 上利用 WebKitGTK,在 Android 上利用 Android System WebView。
Tauri框架流程大致是 Tauri → Wry桥 → Rust
进行分析
跟进查看TauriActivity,我们可以清晰的看到他导入了两个tauri框架的包,他是 Tauri-Android 应用生成的壳 Activity。它把系统的生命周期事件简单地“转发”给 Tauri/Capacitor 的插件体系,让每个插件都有机会同步自己的状态。同时发现它又继承了WryActivity 类。

WryActivity 本身几乎不含业务逻辑;它的作为“桥”,加载了native类,协调在Tauri框架下java层 和native层之间的运行传输,我们需要去跟进查看ez_android_lib
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| import android.annotation.SuppressLint; import android.content.pm.PackageInfo; import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.webkit.WebView; import p058e.AbstractActivityC0854g; import p087o1.AbstractC1272d;
public abstract class WryActivity extends AbstractActivityC0854g {
public RustWebView f3224y;
public static final class Companion { }
static { System.loadLibrary("ez_android_lib"); }
private final native void create(WryActivity wryActivity);
private final native void destroy();
private final native void focus(boolean z2);
private final native void memory();
private final native void onActivityDestroy();
private final native void pause();
private final native void resume();
private final native void save();
private final native void start();
private final native void stop();
public final Class<?> getAppClass(String str) { AbstractC1272d.m3042d("name", str); return Class.forName(str); }
@SuppressLint({"WebViewApiAvailability", "ObsoleteSdkInt"}) public final String getVersion() { PackageInfo currentWebViewPackage; if (Build.VERSION.SDK_INT >= 26) { currentWebViewPackage = WebView.getCurrentWebViewPackage(); String str = currentWebViewPackage != null ? currentWebViewPackage.versionName : null; return str == null ? "" : str; } try { return getPackageManager().getPackageInfo("com.android.chrome", 0).versionName.toString(); } catch (Exception e2) { AbstractC1272d.m3042d("message", "Unable to get package info for 'com.android.chrome'" + e2); try { return getPackageManager().getPackageInfo("com.android.webview", 0).versionName.toString(); } catch (Exception e3) { AbstractC1272d.m3042d("message", "Unable to get package info for 'com.android.webview'" + e3); return ""; } } }
@Override public final void onCreate(Bundle bundle) { super.onCreate(bundle); create(this); }
@Override public final void onDestroy() { super.onDestroy(); destroy(); onActivityDestroy(); }
@Override public final boolean onKeyDown(int i2, KeyEvent keyEvent) { if (i2 == 4) { RustWebView rustWebView = this.f3224y; if (rustWebView == null) { AbstractC1272d.m3045g("mWebView"); throw null; } if (rustWebView.canGoBack()) { RustWebView rustWebView2 = this.f3224y; if (rustWebView2 != null) { rustWebView2.goBack(); return true; } AbstractC1272d.m3045g("mWebView"); throw null; } } return super.onKeyDown(i2, keyEvent); }
@Override public final void onLowMemory() { super.onLowMemory(); memory(); }
@Override public void onPause() { super.onPause(); pause(); }
@Override public void onResume() { super.onResume(); resume(); }
@Override public final void onSaveInstanceState(Bundle bundle) { AbstractC1272d.m3042d("outState", bundle); super.onSaveInstanceState(bundle); save(); }
@Override public final void onStart() { super.onStart(); start(); }
@Override public final void onStop() { super.onStop(); stop(); }
@Override public final void onWindowFocusChanged(boolean z2) { super.onWindowFocusChanged(z2); focus(z2); }
public final void setWebView(RustWebView rustWebView) { AbstractC1272d.m3042d("webView", rustWebView); this.f3224y = rustWebView; } }
|
| Android 回调 |
Java 侧动作 |
JNI 调用 |
onCreate |
create(this) |
初始化 Rust 侧 runtime,传递 Activity 指针 |
onStart |
start() |
页面真正 visible,Rust 可拉起前端逻辑 |
onResume |
resume() |
恢复渲染/JS 通道,可能重启 WebSocket、计时器 |
onWindowFocusChanged |
focus(hasFocus) |
通知焦点变化,决定是否拦截键盘、鼠标 |
onPause |
pause() |
暂停 JS 定时器、媒体、Sensor |
onStop |
stop() |
进入后台,关闭长连接、MediaSession |
onDestroy |
destroy() → onActivityDestroy() |
先释放 Rust 侧资源,再做额外收尾 |
onLowMemory |
memory() |
内存吃紧,让 Rust 清 cache |
onSaveInstanceState |
save() |
备份当前页面状态(history、localStorage 等) |
native层
就像是上面说的,这里使用了rust构建.so文件
但是查看相应的native方法并没有和加密逻辑有关的东西,
根据这个文档:https://blog.yllhwa.com/2023/05/09/Tauri%20%E6%A1%86%E6%9E%B6%E7%9A%84%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%8F%90%E5%8F%96%E6%96%B9%E6%B3%95%E6%8E%A2%E7%A9%B6/
我们知道真正的资源文件已经被brotli 算法进行压缩后再打包。根据博客上的内容找字符串/index.html
这里有很多的/index.html,而我们要找的/index.html是下面有文件内容地址的那个字符串

这里应该是一个结构体,都是以qword形式存在,第一个元素是文件名,第二个是文件名长度,第三个已经被压缩打包的文件内容地址,,第四个是文件内容长度
把内容dump下来写个python脚本进行解包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import brotli import binascii
with open("dump.txt", "r") as f: hex_content = f.read().strip()
print(f"十六进制内容长度: {len(hex_content)}")
try: binary_content = binascii.unhexlify(hex_content) print(f"二进制内容长度: {len(binary_content)} 字节")
try: decompressed = brotli.decompress(binary_content) print(f"解压缩成功!大小: {len(decompressed)} 字节")
with open("dump—_unpack", "wb") as f: f.write(decompressed) print("解压缩数据已写入 'dump' 文件")
try: preview = decompressed[:200].decode('utf-8', errors='ignore') print(f"\n解压缩内容预览:\n{preview}") except: print("\n解压缩的内容似乎是二进制数据")
except brotli.error as e: print(f"Brotli 解压缩失败: {e}")
except binascii.Error as e: print(f"十六进制解码失败: {e}") print("内容可能不是十六进制格式")
print("尝试直接按二进制读取文件...") try: with open("export_results.txt", "rb") as f: binary_content = f.read() print(f"二进制读取成功!大小: {len(binary_content)} 字节")
try: decompressed = brotli.decompress(binary_content) print(f"直接二进制 Brotli 解压缩成功!大小: {len(decompressed)} 字节") with open("dump_direct", "wb") as f: f.write(decompressed) except brotli.error as e: print(f"直接二进制 Brotli 解压缩失败: {e}")
except Exception as e: print(f"二进制读取失败: {e}")
print("\n分析完成。请检查生成的 dump 文件获取结果。")
|
index.html
分析dump下来的html文件,我们发现还要解包一个index-BsFf5qny.js的文件,继续dump+解包+格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Tauri + Vue + Typescript App</title>
<script type="module" crossorigin src="/assets/index-BsFf5qny.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CJgrqWFO.css"> </head>
<body> <div id="app"></div>
</body>
</html>
|
index-BsFf5qny.js
由于解包后的文本比较难阅读,我们可以进行格式化然后分析,分析后实际这个js只是一个瘦前端,功能极简。真正的解题/破解重点在 **Rust 后端命令 **greet 的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ul = Ci({ __name: "App", setup(e) { const t = Qs(""), s = Qs(""); async function n() { t.value = await fl("greet", { flag: s.value }) } return (r, i) => (go(), bo("main", cl, [i[2] || (i[2] = Be("h1", null, "L3HCTF", -1)), Be("form", { class: "row", onSubmit: sl(n, ["prevent"]) }, [Si(Be("input", { id: "greet-input", "onUpdate:modelValue": i[0] || (i[0] = o => s.value = o), placeholder: "Enter a flag..." }, null, 512), [ [ko, s.value] ]), i[1] || (i[1] = Be("button", { type: "submit" }, "Validate", -1))], 32), Be("p", null, Fn(t.value), 1)])) } });
|
greet
该加密流程以 14 字节密钥表为核心,对 27 字节输入逐字节执行“表值异或 → 表值相加 → 受表值控制的 8 位循环位移 → 再异或表值”四步混淆,最终让结果与预置的 3 个 64‑位魔数及第 19 字节比对,通过即视为密钥正确。
表值就是dGhpc2lzYWtleQ
最后三个字节是
丢给ai能直接给出脚本,但是要注意最后三个字节的密文需要自己自行推理推理后是(\x4F\x32\x2A)

exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| S = b"dGhpc2lzYWtleQ"
MAGIC = ( 0x0A409663A025150C.to_bytes(8, 'little') + 0x1FE106294065165C.to_bytes(8, 'little') + 0xFC020A4C0E2C7290.to_bytes(8, 'little') + b'\x4F\x32\x2A' )
def rotr8(x, r): return ((x >> r) | (x << (8 - r))) & 0xFF
def decrypt(buf: bytearray): for i in reversed(range(27)): idx1 = (2 * i + 1) % 14 r = S[(i + 3) % 14] & 7 tmp = buf[i] ^ S[(i + 4) % 14] tmp = rotr8(tmp, r) buf[i] = (tmp - S[idx1]) & 0xFF ^ S[i % 14] return buf
plain = decrypt(bytearray(MAGIC)) print("十六进制:", plain.hex()) print("ASCII :", plain.decode('ascii'))
|
速通结局:
很巧合的你搜索了wrong,你看到了为wrong_answer,直接跟进到了逻辑主要函数,然后直接ai一把梭